2021-07-29

Custom XKB Layout

… in which the author will explain how he managed to create custom key mapping with XKB, why he did it, what you need to know if you want to do it yourself and, most importantly, tell the heroic tale of how he acquired this ancient arcane knowledge.

Let's start with the why.

In the eternal quest of trying to achieve friction-less computing, today I decided to tackle one of the more annoying issues I am facing: Scrolling in Firefox. Or more specifically, how to scroll using the typical hjkl keys I use for navigation in almost all contexts. Scrolling with the arrow keys or the trackpoint isn't really an option, because both are very uncomfortable long-term. It's totally fine when reading a short text, but sometimes I like to scroll through blogs for a few hours.

I have tried a few vim-navigation add-ons for Firefox and was underwhelmed by all of them. Not only do they have horrible input lag (often taken up to a second after the key press to actually scroll), but they also commonly fail on websites that have their own keybinds (like Github) and sometimes even when you just accidentally press Tab and Firefox selects some random element. Furthermore, they work by embedding the website in an <iframe>, which has the tendency to sometimes break things.

My next idea was letting the desktop convert the hjkl key presses to the arrow keys. No desktop I know supports this feature of questionable sanity natively, so my attempt was a bit hacky. I created a new keybind mode with keybinds for hjkl to execute wtype which would then use the virtual-keyboard-unstable-v1 protocol extension to "press" the corresponding arrow key. It did work, however with only marginally better input delay than the browser add-ons.

A custom keyboard with custom firmware is also not an option, since I want this to work not just on my workstation, but also my laptop.

A custom keyboard layout seems to be my last resort.

The research was painful.

<leon-p> I have been looking into custom xkb keymaps for about an hour now.
<leon-p> I think the only apt description is "abandon all hope, ye who enter here".
<leon-p> it probably used to be "here be dragons", but from what I can tell the dragons are long-dead, already fossilized.
<ifreund> oof

XKB is a totally over-engineered very sophisticated system. It is also ancient, originally having been part of the X server. So it will surprise no one that the way keyboard layouts are specified is rather complicated and not very well documented. Actually that is an understatement: I did not find official documentation at all.

Most secondary resources I found online were lackluster. Only a few are worth reading. A lot just pretended this problem can be solved with xmodmap.

These are the sources that did actually help. Since I will only cover the basics needed for my use case here, you probably want to read them if you have the need for further information.

And now what I have found out.

Small disclaimer: I do not claim that everything here is perfectly correct. In fact, it very likely isn't, because I got this information from multiple secondary sources and because I am leaving out a lot. Either way, it does work.

There are many steps between pressing a key on your keyboard and a character appearing on screen, multiple of which are part of XKB. Of those, exactly two are easy to understand and can be described in common words.

When XKB receives a key event from the keyboard driver, it converts the key code into a key symbol. Different drivers on different systems will send different key codes for what is supposed to be the same key, so this needs to be configurable. Have a look at the files in /usr/share/X11/xkb/keycodes/. If you have worked on software using server-side libxkbcommon, like Wayland compositors, you will have already encountered key symbols. Luckily, we do not have to touch this part.

To get from raw key symbols to the events XKB intends for clients, keyboard layouts are used. They are files containing rules defining which key symbols are modifiers and what normal key symbol plus modifier key symbol combination results in what character.

To understand how these rules work, you need to know three terms.

A set of characters belonging to a key symbol is called a group. As an example, the key symbol for the A key could have the group [ a, A ]. Characters in a group are sorted by levels. Here a is Level1 and A is Level2.

A key symbol can have multiple groups. I believe the initial intention was to have separate groups for characters belonging to different alphabets. In our example, this would mean the key symbol belonging to the A key having an additional group [ æ, Æ ]. So for every key symbol you have a (levels.len * groups.len)-matrix of possible characters.

Before you despair, let me quickly add that I am not aware of any layout that actually uses groups. Instead there are simply a few more levels, so our key symbol will in reality only have a single group [ a, A, æ, Æ ] and four levels.

And finally there are types, which are the rules determining which level is currently active based on modifiers. Every key symbol can have an individual type. Our example would have a type that for no modifiers activates Level1, for the Shift modifier activates Level2, for the AltGr modifier activates Level3 and finally for the AltGr+Shift modifier activates Level4. This is probably the most common type, however there are a lot of others in use as well. Have a look at /usr/share/X11/xkb/types/.

It would be masochistic to write a layout entirely from scratch. Luckily, you do not have to do that. The already included layouts are fairly decent and modular and you can just include them in your own. You can find the shipped layouts at /usr/share/X11/xkb/symbols/. There you will find a few base layouts (like latin). The layout that you would actually use are built upon those. As an example, I usually use the nodeadkeys variant of the de layout. That means I am using the de(nodeadkeys) layout, which includes all of and overwrites part of the de layout, which does the same with the latin(type4) layout which does the same with the latin layout.

The things I called "base layouts", like latin, are actually just a partial layouts. They do not describe an entire keyboard layout, just a part of it. To create a complete layout, XKB will pair the partial layout you chose (usually based on your language), with special layouts that define the behaviour of non-alphanumeric key, like the pc layout. For my purposes that is largely uninteresting, so I will not cover it in this article.

When you only want to modify an existing layout, creating a "variant" of it is the path of least resistance.

First, you need to know what the layout you wish to modify is called, because that will also be the name of the file you need to create. My base layout is de, so I need to create ~/.xkb/symbols/de. Note that this local XKB configuration directory does not overwrite the global one. Both ~/.xkb/symbols/de as well as /usr/share/X11/xkb/symbols/de will be parsed.

Here are the contents of my variant file.

01  partial alphanumeric_keys
02  xkb_symbols "lhp" {
03      include "de(nodeadkeys)"
04
05      key.type[Group1] = "FOUR_LEVEL_PLUS_LOCK";
06      key <AC06> { [ h, H, hstroke,       Hstroke,       Left  ] };
07      key <AC07> { [ j, J, dead_belowdot, dead_abovedot, Down  ] };
08      key <AC08> { [ k, K, kra,           ampersand,     Up    ] };
09      key <AC09> { [ l, L, lstroke,       Lstroke,       Right ] };
10  };

What looks a bit like a C function header describes what this layout is supposed to be. partial tells XKB that the rules in the brackets do not describe an entire layout, only part of it. alphanumeric_keys lets XKB which part of the keyboard these rules will affect. xkb_symbols means that the following block is indeed a keyboard layout and "lhp" is what I decided to call my variant.

On line 03 I start the layout by just importing the de(nodeadkeys) layout I usually use, since I do not want to redo all of that myself.

On line 05 I define the type the following key rules should use. The type FOUR_LEVEL_PLUS_LOCK is intended for the ß key on German keyboards. It is a special case, created because there is a capital version of ß that is only used when all letters of a word are capitalized3, so it makes sense to only type that character when caps lock is enabled and reserve Layer2 for something that is used more often, in this case ?. This type has the following levels:

Modifier Key Combination Level Name
None Level1
Shift Level2
AltGr Level3
AltGr+Shift Level4
Caps Level5
Caps+Shift Level2
Caps+AltGr Level3
Caps+AltGr+Shift Level4

Initially I wanted to have a custom type using ScrollLock instead, but I just could not get it to work. After two hours of fiddling with it I decided to go with an existing type and settled on this one.

On lines 06-09 are the key symbols and the group I am assigning to them4. I copied the default German groups for hjkl and simply added a fifth level that act like the arrow keys. I don't have a use for those Level3 and Level4 characters, so I might eventually replace them with more useful ones. I could also just replace them with NoSymbol for the time being, which should be self-explanatory.

Since I am already here, I will overwrite a few other rules as well. For example, the following rules will provide you with an easy way to type ←↑→↓ using AltGr and the arrow keys.

key <UP>    {[ Up,    Up,    uparrow    ]};
key <LEFT>  {[ Left,  Left,  leftarrow  ]};
key <DOWN>  {[ Down,  Down,  downarrow  ]};
key <RIGHT> {[ Right, Right, rightarrow ]};

Note that XKB will infer the correct type for these rules automagically because they are fairly simple.

And finally, let's try to find better keys for {[]}, which are pretty annoying to type on a German keyboard.

key <AC01> {[ a, A, bracketleft  ]};
key <AC02> {[ s, S, bracketright ]};
key <AC03> {[ d, D, braceleft    ]};
key <AC04> {[ f, F, braceright   ]};

By the way, you can also insert unicode characters. If you ever wanted AltGr+c to type the hammer and sickle unicode symbol, the following rule is for you.

key <AB03> {[ c, C, U262D ]};

Conclusion.

As you can see, if you simply want to change a few keys, you can get pretty far with little knowledge. None of the concepts I have encountered here are particularly complicated.

However if you want to do something slightly more complex, like creating your own types, or using groups, you will quickly run into issues, because many concepts are either explained either badly or only partially or not at all. This topic could certainly use some in-depth resources.

Either way, I consider the Firefox scrolling issue to be preliminarily solved. Not just that, I decided to go ahead and have turned the simple navigation-mode I showed off in this article in to a full-blown "lay back, turn on some music and read some blogs"-mode.

Key Re-Map
h, j ,k ,l left, down, up, right
n, p, g page down, page up, home
q, w, e previous track, next track, play/pause

Even better, I now have an easy way to change the special characters I get with the AltGr key, which could turn out to be pretty useful. Eventually I will probably come back and try creating my own custom type once more, but for this year I had enough.

Articles from blogs I read (generated by openring)

whippet lab notebook: guile, heuristics, and heap growth

Greets all! Another brief note today. I have gotten Guile working with one of the Nofl-based collectors, specifically the one that scans all edges conservatively (heap-conservative-mmc / heap-conservative-parallel-mmc). Hurrah!It was a pleasant surprise h…

wingolog, May 22, 2025

Status update, May 2025

Hi! Today wlroots 0.19.0 has finally been released! Among the newly supported protocols, color-management-v1 lays the first stone of HDR support (backend and renderer bits are still being reviewed) and ext-image-copy-capture-v1 enhances the previous screen ca…

emersion, May 15, 2025

Summary of changes for April 2025

Hey everyone!This is the list of all the changes we've done to our projects during the month of April. 100r.co, updated water, ditch bag, woodstove installation, and added new photos and information on first-aid kit. Rabbit Waves, updated Triangular…

Hundred Rabbits, April 30, 2025